// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/icon_util.h"

#include <stddef.h>

#include <vector>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_ptr.h"
#include "base/path_service.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/gfx_paths.h"
#include "ui/gfx/icon_util_unittests_resource.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_family.h"

namespace {

static const char kSmallIconName[] = "icon_util/16_X_16_icon.ico";
static const char kLargeIconName[] = "icon_util/128_X_128_icon.ico";
static const char kTempIconFilename[] = "temp_test_icon.ico";

} // namespace

class IconUtilTest : public testing::Test {
public:
    using ScopedHICON = base::win::ScopedHICON;

    void SetUp() override
    {
        gfx::RegisterPathProvider();
        ASSERT_TRUE(PathService::Get(gfx::DIR_TEST_DATA, &test_data_directory_));
        ASSERT_TRUE(temp_directory_.CreateUniqueTempDir());
    }

    static const int kSmallIconWidth = 16;
    static const int kSmallIconHeight = 16;
    static const int kLargeIconWidth = 128;
    static const int kLargeIconHeight = 128;

    // Given a file name for an .ico file and an image dimensions, this
    // function loads the icon and returns an HICON handle.
    ScopedHICON LoadIconFromFile(const base::FilePath& filename,
        int width,
        int height)
    {
        HICON icon = static_cast<HICON>(LoadImage(NULL,
            filename.value().c_str(),
            IMAGE_ICON,
            width,
            height,
            LR_LOADTRANSPARENT | LR_LOADFROMFILE));
        return ScopedHICON(icon);
    }

    SkBitmap CreateBlackSkBitmap(int width, int height)
    {
        SkBitmap bitmap;
        bitmap.allocN32Pixels(width, height);
        // Setting the pixels to transparent-black.
        memset(bitmap.getPixels(), 0, width * height * 4);
        return bitmap;
    }

    // Loads an .ico file from |icon_filename| and asserts that it contains all of
    // the expected icon sizes up to and including |max_icon_size|, and no other
    // icons. If |max_icon_size| >= 256, this tests for a 256x256 PNG icon entry.
    void CheckAllIconSizes(const base::FilePath& icon_filename,
        int max_icon_size);

protected:
    // The root directory for test files. This should be treated as read-only.
    base::FilePath test_data_directory_;

    // Directory for creating files by this test.
    base::ScopedTempDir temp_directory_;
};

void IconUtilTest::CheckAllIconSizes(const base::FilePath& icon_filename,
    int max_icon_size)
{
    ASSERT_TRUE(base::PathExists(icon_filename));

    // Determine how many icons to expect, based on |max_icon_size|.
    int expected_num_icons = 0;
    for (size_t i = 0; i < IconUtil::kNumIconDimensions; ++i) {
        if (IconUtil::kIconDimensions[i] > max_icon_size)
            break;
        ++expected_num_icons;
    }

    // First, use the Windows API to load the icon, a basic validity test.
    EXPECT_TRUE(LoadIconFromFile(icon_filename, kSmallIconWidth, kSmallIconHeight)
                    .is_valid());

    // Read the file completely into memory.
    std::string icon_data;
    ASSERT_TRUE(base::ReadFileToString(icon_filename, &icon_data));
    ASSERT_GE(icon_data.length(), sizeof(IconUtil::ICONDIR));

    // Ensure that it has exactly the expected number and sizes of icons, in the
    // expected order. This matches each entry of the loaded file's icon directory
    // with the corresponding element of kIconDimensions.
    // Also extracts the 256x256 entry as png_entry.
    const IconUtil::ICONDIR* icon_dir = reinterpret_cast<const IconUtil::ICONDIR*>(icon_data.data());
    EXPECT_EQ(expected_num_icons, icon_dir->idCount);
    ASSERT_GE(IconUtil::kNumIconDimensions, icon_dir->idCount);
    ASSERT_GE(icon_data.length(),
        sizeof(IconUtil::ICONDIR) + icon_dir->idCount * sizeof(IconUtil::ICONDIRENTRY));
    const IconUtil::ICONDIRENTRY* png_entry = NULL;
    for (size_t i = 0; i < icon_dir->idCount; ++i) {
        const IconUtil::ICONDIRENTRY* entry = &icon_dir->idEntries[i];
        // Mod 256 because as a special case in ICONDIRENTRY, the value 0 represents
        // a width or height of 256.
        int expected_size = IconUtil::kIconDimensions[i] % 256;
        EXPECT_EQ(expected_size, static_cast<int>(entry->bWidth));
        EXPECT_EQ(expected_size, static_cast<int>(entry->bHeight));
        if (entry->bWidth == 0 && entry->bHeight == 0) {
            EXPECT_EQ(NULL, png_entry);
            png_entry = entry;
        }
    }

    if (max_icon_size >= 256) {
        ASSERT_TRUE(png_entry);

        // Convert the PNG entry data back to a SkBitmap to ensure it's valid.
        ASSERT_GE(icon_data.length(),
            png_entry->dwImageOffset + png_entry->dwBytesInRes);
        const unsigned char* png_bytes = reinterpret_cast<const unsigned char*>(
            icon_data.data() + png_entry->dwImageOffset);
        gfx::Image image = gfx::Image::CreateFrom1xPNGBytes(
            png_bytes, png_entry->dwBytesInRes);
        SkBitmap bitmap = image.AsBitmap();
        EXPECT_EQ(256, bitmap.width());
        EXPECT_EQ(256, bitmap.height());
    }
}

// The following test case makes sure IconUtil::SkBitmapFromHICON fails
// gracefully when called with invalid input parameters.
TEST_F(IconUtilTest, TestIconToBitmapInvalidParameters)
{
    base::FilePath icon_filename = test_data_directory_.AppendASCII(kSmallIconName);
    gfx::Size icon_size(kSmallIconWidth, kSmallIconHeight);
    ScopedHICON icon(
        LoadIconFromFile(icon_filename, icon_size.width(), icon_size.height()));
    ASSERT_TRUE(icon.is_valid());

    // Invalid size parameter.
    gfx::Size invalid_icon_size(kSmallIconHeight, 0);
    EXPECT_EQ(IconUtil::CreateSkBitmapFromHICON(icon.get(), invalid_icon_size),
        static_cast<SkBitmap*>(NULL));

    // Invalid icon.
    EXPECT_EQ(IconUtil::CreateSkBitmapFromHICON(NULL, icon_size),
        static_cast<SkBitmap*>(NULL));

    // The following code should succeed.
    scoped_ptr<SkBitmap> bitmap;
    bitmap.reset(IconUtil::CreateSkBitmapFromHICON(icon.get(), icon_size));
    EXPECT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
}

// The following test case makes sure IconUtil::CreateHICONFromSkBitmap fails
// gracefully when called with invalid input parameters.
TEST_F(IconUtilTest, TestBitmapToIconInvalidParameters)
{
    ScopedHICON icon;
    scoped_ptr<SkBitmap> bitmap;

    // Wrong bitmap format.
    bitmap.reset(new SkBitmap);
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    bitmap->setInfo(SkImageInfo::MakeA8(kSmallIconWidth, kSmallIconHeight));
    icon = IconUtil::CreateHICONFromSkBitmap(*bitmap).Pass();
    EXPECT_FALSE(icon.is_valid());

    // Invalid bitmap size.
    bitmap.reset(new SkBitmap);
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    bitmap->setInfo(SkImageInfo::MakeN32Premul(0, 0));
    icon = IconUtil::CreateHICONFromSkBitmap(*bitmap).Pass();
    EXPECT_FALSE(icon.is_valid());

    // Valid bitmap configuration but no pixels allocated.
    bitmap.reset(new SkBitmap);
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    bitmap->setInfo(SkImageInfo::MakeN32Premul(kSmallIconWidth,
        kSmallIconHeight));
    icon = IconUtil::CreateHICONFromSkBitmap(*bitmap).Pass();
    EXPECT_FALSE(icon.is_valid());
}

// The following test case makes sure IconUtil::CreateIconFileFromImageFamily
// fails gracefully when called with invalid input parameters.
TEST_F(IconUtilTest, TestCreateIconFileInvalidParameters)
{
    scoped_ptr<SkBitmap> bitmap;
    gfx::ImageFamily image_family;
    base::FilePath valid_icon_filename = temp_directory_.path().AppendASCII(
        kTempIconFilename);
    base::FilePath invalid_icon_filename = temp_directory_.path().AppendASCII(
        "<>?.ico");

    // Wrong bitmap format.
    bitmap.reset(new SkBitmap);
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    // Must allocate pixels or else ImageSkia will ignore the bitmap and just
    // return an empty image.
    bitmap->allocPixels(SkImageInfo::MakeA8(kSmallIconWidth, kSmallIconHeight));
    memset(bitmap->getPixels(), 0, bitmap->width() * bitmap->height());
    image_family.Add(gfx::Image::CreateFrom1xBitmap(*bitmap));
    EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family,
        valid_icon_filename));
    EXPECT_FALSE(base::PathExists(valid_icon_filename));

    // Invalid bitmap size.
    image_family.clear();
    bitmap.reset(new SkBitmap);
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    bitmap->allocPixels(SkImageInfo::MakeN32Premul(0, 0));
    image_family.Add(gfx::Image::CreateFrom1xBitmap(*bitmap));
    EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family,
        valid_icon_filename));
    EXPECT_FALSE(base::PathExists(valid_icon_filename));

    // Bitmap with no allocated pixels.
    image_family.clear();
    bitmap.reset(new SkBitmap);
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    bitmap->setInfo(SkImageInfo::MakeN32Premul(kSmallIconWidth,
        kSmallIconHeight));
    image_family.Add(gfx::Image::CreateFrom1xBitmap(*bitmap));
    EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family,
        valid_icon_filename));
    EXPECT_FALSE(base::PathExists(valid_icon_filename));

    // Invalid file name.
    image_family.clear();
    bitmap->allocPixels();
    // Setting the pixels to black.
    memset(bitmap->getPixels(), 0, bitmap->width() * bitmap->height() * 4);
    image_family.Add(gfx::Image::CreateFrom1xBitmap(*bitmap));
    EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family,
        invalid_icon_filename));
    EXPECT_FALSE(base::PathExists(invalid_icon_filename));
}

// This test case makes sure IconUtil::CreateIconFileFromImageFamily fails if
// the image family is empty or invalid.
TEST_F(IconUtilTest, TestCreateIconFileEmptyImageFamily)
{
    base::FilePath icon_filename = temp_directory_.path().AppendASCII(
        kTempIconFilename);

    // Empty image family.
    EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(gfx::ImageFamily(),
        icon_filename));
    EXPECT_FALSE(base::PathExists(icon_filename));

    // Image family with only an empty image.
    gfx::ImageFamily image_family;
    image_family.Add(gfx::Image());
    EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    EXPECT_FALSE(base::PathExists(icon_filename));
}

// This test case makes sure that when we load an icon from disk and convert
// the HICON into a bitmap, the bitmap has the expected format and dimensions.
TEST_F(IconUtilTest, TestCreateSkBitmapFromHICON)
{
    scoped_ptr<SkBitmap> bitmap;
    base::FilePath small_icon_filename = test_data_directory_.AppendASCII(
        kSmallIconName);
    gfx::Size small_icon_size(kSmallIconWidth, kSmallIconHeight);
    ScopedHICON small_icon(LoadIconFromFile(
        small_icon_filename, small_icon_size.width(), small_icon_size.height()));
    ASSERT_TRUE(small_icon.is_valid());
    bitmap.reset(
        IconUtil::CreateSkBitmapFromHICON(small_icon.get(), small_icon_size));
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    EXPECT_EQ(bitmap->width(), small_icon_size.width());
    EXPECT_EQ(bitmap->height(), small_icon_size.height());
    EXPECT_EQ(bitmap->colorType(), kN32_SkColorType);

    base::FilePath large_icon_filename = test_data_directory_.AppendASCII(
        kLargeIconName);
    gfx::Size large_icon_size(kLargeIconWidth, kLargeIconHeight);
    ScopedHICON large_icon(LoadIconFromFile(
        large_icon_filename, large_icon_size.width(), large_icon_size.height()));
    ASSERT_TRUE(large_icon.is_valid());
    bitmap.reset(
        IconUtil::CreateSkBitmapFromHICON(large_icon.get(), large_icon_size));
    ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL));
    EXPECT_EQ(bitmap->width(), large_icon_size.width());
    EXPECT_EQ(bitmap->height(), large_icon_size.height());
    EXPECT_EQ(bitmap->colorType(), kN32_SkColorType);
}

// This test case makes sure that when an HICON is created from an SkBitmap,
// the returned handle is valid and refers to an icon with the expected
// dimensions color depth etc.
TEST_F(IconUtilTest, TestBasicCreateHICONFromSkBitmap)
{
    SkBitmap bitmap = CreateBlackSkBitmap(kSmallIconWidth, kSmallIconHeight);
    ScopedHICON icon(IconUtil::CreateHICONFromSkBitmap(bitmap));
    EXPECT_TRUE(icon.is_valid());
    ICONINFO icon_info;
    ASSERT_TRUE(GetIconInfo(icon.get(), &icon_info));
    EXPECT_TRUE(icon_info.fIcon);

    // Now that have the icon information, we should obtain the specification of
    // the icon's bitmap and make sure it matches the specification of the
    // SkBitmap we started with.
    //
    // The bitmap handle contained in the icon information is a handle to a
    // compatible bitmap so we need to call ::GetDIBits() in order to retrieve
    // the bitmap's header information.
    BITMAPINFO bitmap_info;
    ::ZeroMemory(&bitmap_info, sizeof(BITMAPINFO));
    bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFO);
    HDC hdc = ::GetDC(NULL);
    int result = ::GetDIBits(hdc,
        icon_info.hbmColor,
        0,
        kSmallIconWidth,
        NULL,
        &bitmap_info,
        DIB_RGB_COLORS);
    ASSERT_GT(result, 0);
    EXPECT_EQ(bitmap_info.bmiHeader.biWidth, kSmallIconWidth);
    EXPECT_EQ(bitmap_info.bmiHeader.biHeight, kSmallIconHeight);
    EXPECT_EQ(bitmap_info.bmiHeader.biPlanes, 1);
    EXPECT_EQ(bitmap_info.bmiHeader.biBitCount, 32);
    ::ReleaseDC(NULL, hdc);
}

// This test case makes sure that CreateIconFileFromImageFamily creates a
// valid .ico file given an ImageFamily, and appropriately creates all icon
// sizes from the given input.
TEST_F(IconUtilTest, TestCreateIconFileFromImageFamily)
{
    gfx::ImageFamily image_family;
    base::FilePath icon_filename = temp_directory_.path().AppendASCII(kTempIconFilename);

    // Test with only a 16x16 icon. Should only scale up to 48x48.
    image_family.Add(gfx::Image::CreateFrom1xBitmap(
        CreateBlackSkBitmap(kSmallIconWidth, kSmallIconHeight)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 48);

    // Test with a 48x48 icon. Should only scale down.
    image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(48, 48)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 48);

    // Test with a 64x64 icon. Should scale up to 256x256.
    image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(64, 64)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 256);

    // Test with a 256x256 icon. Should include the 256x256 in the output.
    image_family.Add(gfx::Image::CreateFrom1xBitmap(
        CreateBlackSkBitmap(256, 256)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 256);

    // Test with a 49x49 icon. Should scale up to 256x256, but exclude the
    // original 49x49 representation from the output.
    image_family.clear();
    image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(49, 49)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 256);

    // Test with a non-square 16x32 icon. Should scale up to 48, but exclude the
    // original 16x32 representation from the output.
    image_family.clear();
    image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(16, 32)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 48);

    // Test with a non-square 32x49 icon. Should scale up to 256, but exclude the
    // original 32x49 representation from the output.
    image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(32, 49)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 256);

    // Test with an empty and non-empty image.
    // The empty image should be ignored.
    image_family.clear();
    image_family.Add(gfx::Image());
    image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(16, 16)));
    ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family,
        icon_filename));
    CheckAllIconSizes(icon_filename, 48);
}

TEST_F(IconUtilTest, TestCreateImageFamilyFromIconResource)
{
    HMODULE module = GetModuleHandle(NULL);
    scoped_ptr<gfx::ImageFamily> family(
        IconUtil::CreateImageFamilyFromIconResource(module, IDR_MAINFRAME));
    ASSERT_TRUE(family.get());
    EXPECT_FALSE(family->empty());
    std::vector<gfx::Image> images;
    for (const auto& image : *family)
        images.push_back(image);

    // Assert that the family contains all of the images from the icon resource.
    EXPECT_EQ(5u, images.size());
    EXPECT_EQ(16, images[0].Width());
    EXPECT_EQ(24, images[1].Width());
    EXPECT_EQ(32, images[2].Width());
    EXPECT_EQ(48, images[3].Width());
    EXPECT_EQ(256, images[4].Width());
}

// This tests that kNumIconDimensionsUpToMediumSize has the correct value.
TEST_F(IconUtilTest, TestNumIconDimensionsUpToMediumSize)
{
    ASSERT_LE(IconUtil::kNumIconDimensionsUpToMediumSize,
        IconUtil::kNumIconDimensions);
    EXPECT_EQ(IconUtil::kMediumIconSize,
        IconUtil::kIconDimensions[IconUtil::kNumIconDimensionsUpToMediumSize - 1]);
}
